package edu.cicese.alz.reports;

import edu.cicese.alz.Query;
import edu.cicese.alz.TimeUtil;
import edu.cicese.alz.util.IncenseUtil;
import edu.cicese.alz.util.PropertyUtil;
import edu.cicese.alz.util.Utilities;

import java.awt.Color;
import java.io.File;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.TreeSet;

/**
 * Created by: Eduardo Quintana Contreras
 * Date: 25/06/12
 * Time: 03:18 PM
 */
public class AccelerometerReport extends Report {
	public static final int TYPE_INTENSITY = 1;
	public static final int TYPE_MAGNITUDE = 2;
	private int type;

	private float alpha;
	private int epochSize;
	private int minTime;

	private Color colorNoData, colorNoMovement, colorSedentary, colorLight, colorModerate, colorHard;
	private Color colorMagnitude;

	private int actSedentary, actLight, actModerate, actHard;

	private final int expectedFrequency = 40;
	private final int maxFrequency = 100;
	private static long timestampAux;

//	private long offset = 3600000;
	private long offset = 0;

	private int[] intensities = {0, 0, 0, 0, 0, 0}; //0: no data 1: no movement 2: sedentary 3: light 4: moderate 5: hard

	private int sedentaryIntensity = 0, lightIntensity = 0, moderateIntensity = 0, hardIntensity = 0;
	private int cLight = 0, cModerate = 0, cHard = 0, cSedentary = 0;

	public AccelerometerReport(TreeSet<Integer> userIDs, Date beginDate, Date endDate, Date dayBeginDate,
	                           Date dayEndDate, boolean showCharts, boolean saveCharts, int type, float alpha) {

		super(userIDs, beginDate, endDate, dayBeginDate, dayEndDate, showCharts, saveCharts);

		this.type = type;
		this.alpha = alpha;
	}


	// Constructor for the Combined Report
	public AccelerometerReport(long tsBegin, long tsEnd, int beginHour, int beginMinute, int beginSecond, int endHour,
	                           int endMinute, int endSecond, int days, int periods, float alpha) {

		super(tsBegin, tsEnd, beginHour, beginMinute, beginSecond, endHour, endMinute, endSecond, days, periods);

		this.alpha = alpha;
	}

	/*public AccelerometerReport(TreeSet<Integer> userIDs,
	                 Date date, Date dayBegin, Date dayEnd) {
		super(userIDs, beginDate, endDate, dayBeginDate, dayEndDate);
		this.userIDs = userIDs;

		this.tsBegin = TimeUtil.getAdjustedTimestamp(date.getTime(), beginHour, beginMinute, beginSecond);
		this.tsEnd = TimeUtil.getAdjustedTimestamp(date.getTime(), endHour, endMinute, endSecond);

		type = TYPE_MAGNITUDE;
	}*/

	public void setIntensitySettings(int epoch, int minTime,
	                                 Color colorNoData, Color colorNoMovement, Color colorSedentary,
	                                 Color colorLight, Color colorModerate, Color colorHard,
	                                 int actSedentary, int actLight, int actModerate, int actHard) {

		setIntensitySettings(epoch, minTime, actSedentary, actLight, actModerate, actHard);

		this.colorNoData = colorNoData;
		this.colorNoMovement = colorNoMovement;
		this.colorSedentary = colorSedentary;
		this.colorLight = colorLight;
		this.colorModerate = colorModerate;
		this.colorHard = colorHard;
	}

	public void setIntensitySettings(int epoch, int minTime,
	                                 int actSedentary, int actLight, int actModerate, int actHard) {
		this.epochSize = epoch;
		this.minTime = minTime;
		this.actSedentary = actSedentary;
		this.actLight = actLight;
		this.actModerate = actModerate;
		this.actHard = actHard;
	}

	public void setMagnitudeSettings(Color colorMagnitude) {
		this.colorMagnitude = colorMagnitude;
	}


	public void createCharts() {
		String outputDir = "";
		if (saveCharts) {
			outputDir = IncenseUtil.getReportChartsDir("acc");
		}

		AccelerometerCount.setAlpha(alpha);

		for (int userID : userIDs) {
			if (type == TYPE_INTENSITY) {
				getActivityCounts(userID);
				plot(userID, getBlockValues(userID), outputDir);
			} else {
				//TODO Chart magnitude
			}
		}
	}

	protected void getActivityCounts(int userID) {
		Calendar cal = TimeUtil.getDayTimestamp(tsBegin);
		long dayTimestamp = cal.getTimeInMillis();

		dayTimestamp += offset;

		int endHourAux = endMinute == 0 ? endHour : endHour + 1;

		for (int day = 0; day < days; day++) {
			for (int hour = beginHour; hour <= endHourAux - 1; hour++) {
				// Check if this hour counts were already computed
				if (!isComputed(userID, dayTimestamp, hour)) {

					long hourTimestamp = dayTimestamp + (hour * 3600000);

					ResultSet resultSet = Query.executeQuery(
							queryAccelerometerData(userID, hourTimestamp, hourTimestamp + 3600000));

					getHourCounts(userID, resultSet, dayTimestamp, hourTimestamp, hour);

					Query.close();
				}
				else {
//					System.out.println("Day " + day + ", hour " + hour);
					getAccelerometerTables(userID, dayTimestamp + (hour * 3600000), dayTimestamp + (hour * 3600000) + 3600000);
				}
			}

//			mergeCounts(userID, dayTimestamp);

			cal.add(Calendar.DAY_OF_MONTH, 1);
			dayTimestamp = cal.getTimeInMillis() + offset;
		}
	}

	private int counter = 0;

	protected double[][] getBlockValues(int userID) {
		// Create plot
		Calendar cal = TimeUtil.getDayTimestamp(tsBegin);
		long dayTimestamp = cal.getTimeInMillis();

		dayTimestamp += offset;

		double[][] values = new double[3][periods * days];
		Queue<Intensity> queue = new LinkedList<>();
		initiateQueue(queue);

		int[] intensityTypes = {4, 3, 2, 1}; //{"H", "M", "L", "S"}

		for (int d = 0; d < days; d++) {

			clearIntensities();
//			counter = 0;

			double[] valuesAux = getPeriodCounts(userID, dayTimestamp);

			Intensity[] intensityArray = new Intensity[1440];

			for (int p = 0; p < periods; p++) {
				values[0][d * periods + p] = dayTimestamp;
				values[1][d * periods + p] = p * PERIOD_LENGTH + startTime;
				double intensity = getIntensity(valuesAux[p + beginMinute]);
				intensityArray[p] = new Intensity(intensity);

				values[2][d * periods + p] = intensity;
			}

			for (int intensityType : intensityTypes) {
				counter = 0;
				for (int p = 0; p < periods; p++) {
					addToQueue(queue, intensityArray[p], intensityType);
				}
			}


			/*for (int p = 0; p < periods; p++) {
				values[0][d * periods + p] = dayTimestamp;
				values[1][d * periods + p] = p * PERIOD_LENGTH + startTime;
				double intensity = getIntensity(valuesAux[p + beginMinute]);
				addToQueue(queue, intensity);
				values[2][d * periods + p] = intensity;
//				System.out.println(TimeUtil.getStringDateFromMs((long) (values[0][d * periods] + values[1][d * periods + p]), TimeUtil.sdf) + " " + values[2][d * periods + p]);
			}*/

			System.out.println(
					TimeUtil.getStringDateFromMs(dayTimestamp, TimeUtil.usaFormat) + "\t" +
							cSedentary + "\t" + "\t" +
							cLight + "\t" + "\t" +
							cModerate + "\t" + "\t" +
							cHard + "\t" + "\t" +
							intensities[0] + "\t" + (1 - intensities[0] / 1440.0) + "\t" +
							intensities[1] + "\t" +
							intensities[2] + "\t" +
							intensities[3] + "\t" +
							intensities[4] + "\t" +
							intensities[5]
			);

			cal.add(Calendar.DAY_OF_MONTH, 1);
			dayTimestamp = cal.getTimeInMillis();
			dayTimestamp += offset;

		}

		return values;
	}

	public void plot(int userID, double[][] values, String outputDir) {
		FrameAccelerometerBlock frameAccelerometerBlock = new FrameAccelerometerBlock(
				userID, startTime, colorNoData, colorNoMovement, colorSedentary, colorLight, colorModerate, colorHard);

		if (saveCharts) {
			frameAccelerometerBlock.setOutputDir(outputDir);
		}

		frameAccelerometerBlock.plotData(values, days);

		if (showCharts) {
			frameAccelerometerBlock.pack();
			frameAccelerometerBlock.setVisible(true);
		} else {
			frameAccelerometerBlock.dispose();
		}

	}

	private void mergeCounts(int userID, long dayTimestamp) {
		String filename = getDayDir(userID, dayTimestamp) + dayTimestamp + ".ct";
		int period = 0;

		int endHourAux = endMinute == 0 ? endHour : endHour + 1;

		double[] counts = new double[(endHourAux - beginHour) * 60];
		for (int hour = beginHour; hour <= endHourAux - 1; hour++) {
			double[] hourCounts = (double[]) IncenseUtil.loadData(filename + Utilities.addZeros(hour, 2));
			if (hourCounts != null) {
				for (int i = 0; i < 60; i++, period++) {
					counts[period] = hourCounts[i];
				}
			} else {
				period += 60;
			}
		}

		// Save merged file
		IncenseUtil.saveToFile(
				counts,
				getDayDir(userID, dayTimestamp),
				dayTimestamp + ".ct");
	}

	private double[] getPeriodCounts(int userID, long dayTimestamp) {
		String filename = getDayDir(userID, dayTimestamp) + dayTimestamp + ".ct";

		int period = 0;
		int endHourAux = endMinute == 0 ? endHour : endHour + 1;
		double[] counts = new double[(endHourAux - beginHour) * 60];

		for (int hour = beginHour; hour <= endHourAux - 1; hour++) {
			double[] hourCounts = (double[]) IncenseUtil.loadData(filename + Utilities.addZeros(hour, 2));
			if (hourCounts != null) {
				for (int i = 0; i < 60; i++, period++) {
					counts[period] = hourCounts[i];
				}
			} else {
				period += 60;
			}
		}
		return counts;
	}

	public double[] getHourCounts(int userID, ResultSet resultSet, long dayTimestamp, long hourTimestamp, int hour) {
		List<AccelerometerMeasure> accelerometerMeasures = new ArrayList<>();

		try {
			//TODO: CHECK!!!!!
			if (resultSet != null) {
				while (resultSet.next()) {
					double axisX = resultSet.getDouble(1);
					double axisY = resultSet.getDouble(2);
					double axisZ = resultSet.getDouble(3);
					accelerometerMeasures.add(new AccelerometerMeasure(
							axisX,
							axisY,
							axisZ,
							(int) StrictMath.round(Math.sqrt(Math.pow(axisX, 2) + Math.pow(axisY, 2) + Math.pow(axisZ, 2))),
							resultSet.getLong(4)));
				}
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}

		int periods = 60;
		double[] counts = new double[periods];    //activity counts

		if (!accelerometerMeasures.isEmpty()) {
			long tsAux = hourTimestamp;

			for (int period = 0; period < periods; period++) {
				counts[period] = getCount(accelerometerMeasures, tsAux);
				tsAux += PERIOD_LENGTH;
			}
		} else {
			for (int period = 0; period < periods; period++) {
				counts[period] = -1;
			}
		}

		IncenseUtil.saveToFile(counts,
				getDayDir(userID, dayTimestamp),
				dayTimestamp + ".ct" + Utilities.addZeros(hour, 2));

		return counts;
	}

	private double getCount(List<AccelerometerMeasure> accMeasures, long tsAux) {
		long tsA = timestampAux = tsAux;
		long tsB = tsAux + PERIOD_LENGTH;

		double count = -1;
		long mTimestamp;

		int accFilteredMagnitudesRounded = 0;
		double[] accFM;

		int freqForAvg = 0, counter = 0;
		long frequency, invalidFrequency = 0;

		if (!accMeasures.isEmpty()) {
			while (!accMeasures.isEmpty()) {
				mTimestamp = accMeasures.get(0).getTimestamp();

				if (mTimestamp > tsB) {
					break;
				}

				if (mTimestamp > tsA) {
					frequency = mTimestamp - timestampAux;
					if (frequency > maxFrequency) {
						invalidFrequency += -expectedFrequency + frequency;
					} else {
						freqForAvg += frequency;
						counter++;
					}

					accFM = AccelerometerCount.getFilteredAcceleration(
							accMeasures.get(0).getAxisX(), accMeasures.get(0).getAxisY(), accMeasures.get(0).getAxisZ());
					accFilteredMagnitudesRounded += StrictMath.round(Math.sqrt(
							Math.pow(accFM[0], 2) +
									Math.pow(accFM[1], 2) +
									Math.pow(accFM[2], 2)));

					timestampAux = mTimestamp;
				}

				accMeasures.remove(0);
			}

			if (counter != 0) {
				float freqAvg = (float) freqForAvg / counter;
				float freqRatio = (float) expectedFrequency / freqAvg;
				long time = PERIOD_LENGTH - invalidFrequency;

				count = ((float) accFilteredMagnitudesRounded / (((float) time / 1000) / 60)) / freqRatio;
			}
		}

		return count;
	}

	private double getIntensity(double count) {
		if (count == -1) {
			//no data
			intensities[0]++;
			return 0.5;
		}
		if (count < actSedentary) {
			//no movement
			intensities[1]++;
			return 1.5;
		}
		if (count < actLight) {
			//sedentary
			intensities[2]++;
			return 2.5;
		}
		if (count < actModerate) {
			//light
			intensities[3]++;
			return 3.5;
		}
		if (count < actHard) {
			//moderate
			intensities[4]++;
			return 4.5;
		}
		//hard
		intensities[5]++;
		return 5.5;
	}

	private boolean isComputed(int userID, long dayTimestamp, int hour) {
		String dir = PropertyUtil.getAccelerometerCountsDir(userID) + alpha + File.separator
				+ dayTimestamp + File.separator;
		if (!new File(dir).exists()) {
			return false;
		} else {
			/*String filename = dir + dayTimestamp + ".ct" + Utilities.addZeros(hour, 2);
			if (!new File(filename).exists()) {
				return false;
			}
			else {
				double[] data = (double[]) IncenseUtil.loadData(filename);
				//
			}*/
			return new File(dir + dayTimestamp + ".ct" + Utilities.addZeros(hour, 2)).exists();
		}
	}


	private String getDayDir(int userID, long dayTimestamp) {
		return PropertyUtil.getAccelerometerCountsDir(userID) + alpha + File.separator + dayTimestamp + File.separator;
	}


	private void initiateQueue(Queue<Intensity> queue) {
		queue.clear();
		for (int i = 1; i <= epochSize; i++) {
			queue.add(new Intensity(0.5));
		}
		sedentaryIntensity = 0;
		lightIntensity = 0;
		moderateIntensity = 0;
		hardIntensity = 0;
	}

	private void addToQueue(Queue<Intensity> queue, Intensity intensity, int type) {
		counter++;

		queueModified(queue.poll().intensity, -1);
		queue.add(intensity);
		queueModified(intensity.intensity, 1);

//		boolean hasInBout = false;
		int inBout = 0;
		for (Intensity i : queue) {
			if (i.inBout) {
				inBout++;
			}
		}

		if (inBout <= 2 && counter >= 10) {
			checkValues(queue, type);
			counter = 0;
		}

//		if (counter >= 10) {
//			checkValues(queue);
//			counter = 0;
//		}
	}

	private void queueModified(double value, int aux) {
		if (value == 1.5 || value == 2.5) {
			sedentaryIntensity += aux;
			return;
		} else if (value == 3.5) {
			lightIntensity += aux;
			return;
		} else if (value == 4.5) {
			moderateIntensity += aux;
			return;
		} else if (value == 5.5) {
			hardIntensity += aux;
		}
	}

	/*private void addToQueue(Queue<Double> queue, double intensity) {
		counter++;
		queueModified(queue.poll(), -1);
		queue.add(intensity);
		queueModified(intensity, 1);
		if (counter >= 10) {
			checkValues(queue);
			counter = 0;
		}
	}

	private void queueModified(double value, int aux) {
		if (value == 1.5 || value == 2.5) {
			sedentaryIntensity += aux;
			return;
		} else if (value == 3.5) {
			lightIntensity += aux;
			return;
		} else if (value == 4.5) {
			moderateIntensity += aux;
			return;
		} else if (value == 5.5) {
			hardIntensity += aux;
		}
	}*/

	private void checkValues(Queue<Intensity> queue, int type) {
		boolean initiate = false;

		switch (type) {
			// hard
			case 4:
				if (hardIntensity >= minTime) {
					cHard++;
					initiate = true;

//					queue.clear();
//					for (int i = 1; i <= epochSize - minTime; i++) {
//						queue.poll();
////						queue.add(new Intensity(0.5));
//					}
//					sedentaryIntensity = 0;
//					lightIntensity = 0;
//					moderateIntensity = 0;
//					hardIntensity = 0;
				}
				break;
			// moderate
			case 3:
				if (moderateIntensity + hardIntensity >= minTime) {
					cModerate++;
					initiate = true;
				}
				break;
			// light
			case 2:
				if (lightIntensity + moderateIntensity + hardIntensity >= minTime) {
					cLight++;
					initiate = true;
				}
				break;
			// sedentary
			case 1:
				if (sedentaryIntensity + lightIntensity + moderateIntensity + hardIntensity >= minTime) {
					cSedentary++;
					initiate = true;
				}
				break;
		}
//		boolean initiate = false;
//
//		if (hardIntensity >= minTime) {
//			cHard++;
//			initiate = true;
//		} else if (moderateIntensity + hardIntensity >= minTime) {
//			cModerate++;
//			initiate = true;
//		} else if (lightIntensity + moderateIntensity + hardIntensity >= minTime) {
//			cLight++;
//			initiate = true;
//		} else if (sedentaryIntensity + lightIntensity + moderateIntensity + hardIntensity >= minTime) {
//			cSedentary++;
//			initiate = true;
//		}

//		if (lightIntensity >= minTime) {
//			cLight++;
//		} else if (moderateIntensity >= minTime) {
//			cModerate++;
//		} else if (hardIntensity >= minTime) {
//			cHard++;
//		}
		if (initiate) {
			for (Intensity intensity : queue) {
				intensity.inBout = true;
			}
			initiateQueue(queue);
		}
	}

	/*private void checkValues(Queue<Double> queue) {
		boolean initiate = false;
		if (lightIntensity >= minTime) {
			cLight++;
			initiate = true;
		} else if (moderateIntensity >= minTime) {
			cModerate++;
			initiate = true;
		} else if (hardIntensity >= minTime) {
			cHard++;
			initiate = true;
		}
		if (initiate) {
			initiateQueue(queue);
		}
	}*/

	private void printCount() {
		System.out.println(cSedentary + " " + cLight + " " + cModerate + " " + cHard);
	}


	//Use intervals of 1 hour (memory issues)
	private String queryAccelerometerData(int userID, long timestamp1, long timestamp2) {
		String query = "";
		List<Integer> accelerometerTables = getAccelerometerTables(userID, timestamp1, timestamp2);

		if (accelerometerTables.size() > 0) {
			if (accelerometerTables.size() > 2) {
				System.out.println("accelerometerTables.size() = " + accelerometerTables.size() + ". Why? Repeated " +
						"timestamps? Wrong infiles? What?!");
			} else {
				int firstTable = accelerometerTables.get(0);
				int lastTable = accelerometerTables.get(accelerometerTables.size() - 1);

//				System.out.println(firstTable + " " + lastTable);

				for (int tableID = firstTable; tableID <= lastTable; tableID++) {
					query += "SELECT AXIS_X, AXIS_Y, AXIS_Z, TIMESTAMP\n" +
							"FROM accelerometer_" + userID + "_" + tableID + "\n";
					if (tableID == firstTable || tableID == lastTable) {
						query += "WHERE TIMESTAMP >= " + timestamp1 + " AND TIMESTAMP <= " + timestamp2 + "\n";
					}
					if (tableID != lastTable) {
						query += "UNION\n";
					}
				}
			}
		} else {
			Utilities.printLog("No accelerometer data for given period. User: " + userID + ", from: " +
					TimeUtil.getStringDateFromMs(timestamp1, TimeUtil.sdf) + " to: " +
					TimeUtil.getStringDateFromMs(timestamp2, TimeUtil.sdf));
		}
		return query;
	}

	private List<Integer> getAccelerometerTables(int userID, long timestamp1, long timestamp2) {
		List<Integer> accelerometerTables = new ArrayList<>();
		try {
			ResultSet resultSet = Query.executeQuery(Query.queryUserTables(userID));
			int tables = 0;
			if (resultSet.next()) {
				tables = resultSet.getInt(1);
			}

			if (tables > 0) {
				long first = 0, last = 0;
				resultSet = Query.executeQuery(Query.querySelectTableLimit(userID, "FIRST", 1));
				if (resultSet.next()) {
					first = resultSet.getLong(1);
				}

				if (timestamp1 < first) {
					accelerometerTables.add(1);
				} else {
					resultSet = Query.executeQuery(Query.querySelectTableID(userID, timestamp1));
					if (resultSet.next()) {
						accelerometerTables.add(resultSet.getInt(1));
					}
				}

				resultSet = Query.executeQuery(Query.querySelectTableLimit(userID, "LAST", tables));
				if (resultSet.next()) {
					last = resultSet.getLong(1);
				}

				if (timestamp2 > last) {
					accelerometerTables.add(tables);
				} else {
					resultSet = Query.executeQuery(Query.querySelectTableID(userID, timestamp2));
					if (resultSet.next()) {
						accelerometerTables.add(resultSet.getInt(1));
					}
				}
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			Query.close();
		}

//		System.out.println("Tables: " + accelerometerTables);

		return accelerometerTables;
	}

	private void clearIntensities() {
		intensities = new int[]{0, 0, 0, 0, 0, 0};
	}
}
